#!/usr/bin/python
# LRC lyric format parser (c) 2007 hashao
# This file is licensed under the GPL version 3 or later.
# $Revision$ by $Author$
# $HeadURL$

__version__ = "0.0.1"
DEBUG=1

import sys
import datetime
import re
import bisect

# regex for [mm:ss.xx] or [id:tag text] 
boxlabels = re.compile(r'^(\[[^]]+:[^]]*\]\s*)+')
boxlabel_single = re.compile(r'\[([^]]+:[^]]*)\]')

# regex for <mm:ss.xx> in Enhanced LRC
anglelabel = re.compile(r'(<\d+:[.:0-9]*>)')

# Split time format from dd:mm:ss.xx
timefmt = re.compile(r'(\d*:)?(\d+):(\d+)\.?(\d+)?')

class Lrc(object):
    """LRC lyric file parser.
    Lrc(fname, encoding="utf-8")

       fname: .lrc filename,
       encoding: lyric encoding.
       elrc: Flag for Enhanced LRC sub-labels:
            None: Do nothing. Lyric as string.
            "split": Split time and labels. Lyric as list of [<mm:ss.xx>, lyric,...]
                     Same as None when not ELRC label present.
            "remove": Remove all the time labels. Lyric as string.
    """
    def __init__(self, fname="", encoding="utf-8", elrc=None):
        self._fname = fname
        self.encoding = encoding
        self.elrc = elrc
        self._lyric_tree = {}
        self._time_line = []
        self.tags = {}


    def reset(self):
        """Reset Lrc class to its initial state."""
        self._lyric_tree = {}
        self._time_line = []
        self.tags = {}

    def split_elrc(self, text):
        "Split an ELRC line into list, if not elrc, return unchanged."
        stext = anglelabel.split(text)
        # Only valid if we have ELRC tags.
        if len(stext) > 1:
            if not stext[0]: del stext[0]
            if not stext[-1]: del stext[-1]
            text = stext
        return text

    def parse_line(self, line):
        """Parse one line in a lyric file."""

        #find labels
        matchall = boxlabels.search(line)
        #print boxlabel.findall(line)

        # split multiple labels like [xx:xx][yy:yy]text...
        stamps = boxlabel_single.findall(matchall.group())

        text = line[matchall.end():]
        # Treat ELRC <mm:ss.xx>words<mm:ss.xx>words...<mm:ss.xx>
        if text:
            if self.elrc == "remove":
                text = anglelabel.sub('', text)
            elif self.elrc == "split":
                text = self.split_elrc(text)
            # print 'ELRC', text

        tag, content = stamps[0].split(':', 1)
        if tag.isdigit():
            timeline = []
            for stamp in stamps:
                tlabel = timefmt.match(stamp)
                # print tlabel.groups()

                lt = []
                for l in tlabel.groups():
                    if l is None:
                        l = '0'
                    lt.append(int(l))
                # print lt
                lt[3] = lt[3]*10

                # timedelta is used for easy sort and comparison.
                timeline.append(datetime.timedelta(hours=lt[0], minutes=lt[1],
                    seconds=lt[2], milliseconds = lt[3]))

            for t in timeline:
                self._lyric_tree[t] = text
        else:
            # id3tags
            self.tags[tag] = content

    def _parse_iter(self, iter):
        """Parse lines in an iterator."""
        parse_line = self.parse_line
        for line in iter:
            try:
                if line.startswith('['):
                    parse_line(line.strip())
            except AttributeError:
                if DEBUG:
                    print sys.exc_info()[1]
                    print line
                    raise
        # Always save a copy of timeline for fast access.
        self._time_line = sorted(self._lyric_tree.keys())

    def parse_stream(self, fobj):
        """Parse a file like stream"""
        self._parse_iter(fobj)

    def parse(self, afile=None):
        """Parse the lyric file."""
        if afile:
            self._fname = afile
        fobj = open(self._fname)
        self.parse_stream(fobj)

    def parse_text(self, text):
        """Parse strings from buffer."""
        lines = text.splitlines()
        self._parse_iter(lines)

    def _get_time_line_idx(self, time):
        """Return a idx into the self._time_line for any given time."""
        tid = bisect.bisect_left(self._time_line, time)
        if tid >= len(self._time_line):
            tid = len(self._time_line)-1
        return tid

    def get_time_id(self, time):
        """Return a valid line time label given an arbitory time."""
        """If the give time is bigger than the max time, the last 
        max time will be returned."""
        tid = self._get_time_line_idx(time)
        return self._time_line[tid]

    def get_lyric(self, time):
        """Return a line of lyric at the given time."""
        tid = self.get_time_id(time)
        return self._lyric_tree[tid]

    def get_iter(self, time):
        """Iterate over a lyric tree from a give start point."""
        tid = self._get_time_line_idx(time)
        for i in range(tid, len(self._time_line)):
            lt = self._time_line[i]
            yield (lt, self._lyric_tree[lt])

    def __iter__(self):
        """Iterator method."""
        return self.get_iter(datetime.timedelta(0))

    def get_time_line(self):
        """Return a list of all the time index in a lyric."""
        return self._time_line[:]

    def get_lyric_tree(self):
        """Return a copy of lyric parse tree."""
        return self._lyric_tree.copy()

    def __len__(self):
        """Total number of lines in the lyric."""
        return len(self._time_line)

    def get_duration(self):
        """Play duration of the lyric."""
        # FIXME: Should be: last - 0?
        return self._time_line[-1] - self._time_line[0]

    def __str__(self):
        """Pretty print."""
        result = []
        for k, v in self.tags.items():
            result.append('%s: %s' % (k, v))
        for k in sorted(self._lyric_tree.keys()):
            result.append('%s: %s' % (k, self._lyric_tree[k]))
        return '\n'.join(result)

def delta2sec(d):
    """Convert a timedelta into seconds."""
    if d.days == 0:
        if d.microseconds == 0:
            return d.seconds
        else:
            return d.seconds + d.microseconds/1000000.0
    else:
        return d.days*24*3600 + d.seconds + d.microseconds/1000000.0

def timelabel2delta(timelabel):
    """Convert a time label in hh:mm:ss.xxx into timedelta."""
    ndigit = 3
    model = [0]*ndigit # A template for timedelta[0, 0, 0.0]

    tlist = [float(x) for x in timelabel.lstrip(':').split(':')]
    for i in range(1,len(tlist)+1):
        model[-i] = tlist[-i]
    dtime = datetime.timedelta(seconds=model[-1], minutes=model[-2],
            hours=model[-3])
    return dtime

def main():
    #if False: 
    if len(sys.argv) == 1:
        t = timelabel2delta("23:12.33")
        print t, delta2sec(t)
        t = timelabel2delta("12.33")
        print t, delta2sec(t)
        t = timelabel2delta("0:12.33")
        print t, delta2sec(t)
        t = timelabel2delta("2:0:12.34")
        print t, delta2sec(t)
        return

    files = sys.argv[1:]

    for f in files:
        lrc = Lrc(f, elrc='split')
        lrc.parse()
        print lrc
        # Test get_time_id at 1 minute and overflow line.
        tid = lrc.get_time_id(datetime.timedelta(0))
        print tid, lrc._lyric_tree[tid]
        tid = lrc.get_time_id(datetime.timedelta(minutes=1))
        print tid, lrc._lyric_tree[tid]
        tid = lrc.get_time_id(datetime.timedelta(hours=1))
        print tid, lrc._lyric_tree[tid]
        # print lrc._time_line
        for i in lrc:
            print i[0], i[1]
        print '***2 minutes mark:'
        for i in lrc.get_iter(datetime.timedelta(minutes=2)):
            print i[0], i[1]
        print 'lines:', len(lrc), 'duration:', delta2sec(lrc.get_duration())

if __name__ == '__main__':
    main()

# vim:ts=8:sw=4:expandtab

